﻿using System;
using System.Collections.Generic;
using System.Text;
using System.IO;

namespace DeflateLib
{
    public class DeflateHintStream : Stream
    {
        Stream deflateStream;
        Stream hintStream;
        long hintStreamOffset;

        long length;
        long position;

        byte[] page = null;
        int pageIndex = -1;

        HeaderInformation header;

        const int HeaderLength = DeflateHintCreator.HeaderLength;
        static byte[] Signature = DeflateHintCreator.Signature;

        public override bool CanRead
        {
            get { return true; }
        }

        public override bool CanSeek
        {
            get { return true; }
        }

        public override bool CanWrite
        {
            get { return false; }
        }

        public override void Flush()
        {
            throw new NotImplementedException();
        }

        public override long Length
        {
            get { return length; }
        }

        public override long Position
        {
            get
            {
                return position;
            }
            set
            {
                Seek(value, SeekOrigin.Begin);
            }
        }
        
        public DeflateHintStream(Stream deflateStream, Stream hintStream)
        {
            if (deflateStream == null) throw new ArgumentNullException("deflateStream");
            if (hintStream == null) throw new ArgumentNullException("hintStream");

            this.deflateStream = deflateStream;
            this.hintStream = hintStream;
            this.hintStreamOffset = hintStream.Position;

            ReadHeader();

            this.length = header.UncompressedSize;
            this.position = 0;
        }

        private void ReadHeader()
        {
            byte[] headerData = new byte[HeaderLength];
            if (hintStream.Read(headerData, 0, headerData.Length) != HeaderLength)
                throw new InvalidOperationException("Invalid header");

            if (headerData[0] != Signature[0] || headerData[1] != Signature[1] ||
                headerData[2] != Signature[2] || headerData[3] != Signature[3])
                throw new InvalidOperationException("Invalid header signature");


            HeaderInformation header = new HeaderInformation();
            header.HintInterval = BitConverter.ToInt32(headerData, 4);
            header.PointersOffset = BitConverter.ToInt64(headerData, 8);
            header.UncompressedSize = BitConverter.ToInt64(headerData, 16);
            header.StreamOffset = BitConverter.ToInt64(headerData, 24);

            this.header = header;
        }

        public override int Read(byte[] buffer, int offset, int count)
        {
            // TODO parameter checks
            if (count <= 0) return count;

            if(position >= length) return 0;

            int canRead = count > (length - position) ? (int)(length - position) : count;

            long pageStartPosition = header.HintInterval * pageIndex;
            long pageEndPosition = pageStartPosition;
            if(page != null) pageEndPosition += page.Length;

            if (position < pageStartPosition || position + canRead > pageEndPosition)
            {
                // whole data not in the current page
                byte[] cachedPage = this.page;
                int cachedPageIndex = this.pageIndex;

                // try all pages
                int startPageIndex = (int)(position / header.HintInterval);
                int lastPageIndex = (int)((position + canRead - 1) / header.HintInterval);

                if (startPageIndex != cachedPageIndex)
                {
                    this.page = GetHintData(startPageIndex);
                    this.pageIndex = startPageIndex;
                }
                int startPageOffset = (int)(position - startPageIndex * header.HintInterval);
                int startPageTail = page.Length - startPageOffset;
                if (startPageTail >= canRead)
                {
                    Array.Copy(page, startPageOffset, buffer, offset, canRead);
                    position += canRead;
                }
                else
                {
                    Array.Copy(page, startPageOffset, buffer, offset, startPageTail);
                    offset += startPageTail;
                    position += startPageTail;
                    int leftToRead = canRead - startPageTail;
                    for (int i = startPageIndex + 1; i < lastPageIndex; i++)
                    {
                        if (cachedPageIndex == i)
                        {
                            this.page = cachedPage;
                            this.pageIndex = cachedPageIndex;
                        }
                        else
                        {
                            this.page = GetHintData(i);
                            this.pageIndex = i;
                        }
                        Array.Copy(page, 0, buffer, offset, page.Length);
                        offset += page.Length;
                        position += page.Length;
                        leftToRead -= page.Length;
                    }

                    if (cachedPageIndex == lastPageIndex)
                    {
                        this.page = cachedPage;
                        this.pageIndex = cachedPageIndex;
                    }
                    else
                    {
                        this.page = GetHintData(lastPageIndex);
                        this.pageIndex = lastPageIndex;
                    }

                    Array.Copy(page, 0, buffer, offset, leftToRead);
                    position += leftToRead;
                }
            }
            else // whole data in the page
            {
                Array.Copy(page, (int)(position - pageStartPosition), buffer, offset, canRead);
                position += canRead;
            }

            return canRead;
        }

        public override int ReadByte()
        {
            if (position >= length) return -1;

            int positionPageIndex = (int)(position / header.HintInterval);
            if(pageIndex == positionPageIndex)
            {
                int result = page[position - positionPageIndex * header.HintInterval];
                position++;
                return result;
            }
            else
                return base.ReadByte();
        }

        public override long Seek(long offset, SeekOrigin origin)
        {
            switch (origin)
            {
                case SeekOrigin.Begin: position = offset; break;
                case SeekOrigin.End: position = length + offset; break;
                case SeekOrigin.Current: position = position + offset; break;
            }
            if (position < 0) throw new InvalidOperationException("Cannot position before the begin");
            if (position > length ) throw new InvalidOperationException("Cannot position after the end");

            return position;
        }

        public override void SetLength(long value)
        {
            throw new NotImplementedException();
        }

        public override void Write(byte[] buffer, int offset, int count)
        {
            throw new NotImplementedException();
        }

        private byte[] GetHintData(int index)
        {
            MemoryStream ms = new MemoryStream();

            const int PointerDataSize = 8;

            long pointerOffset = header.PointersOffset
                    + index * PointerDataSize;
            byte[] pointerData = new byte[PointerDataSize];

            hintStream.Position = pointerOffset;
            hintStream.Read(pointerData, 0, PointerDataSize);

            long dataOffset = BitConverter.ToInt64(pointerData, 0);
            hintStream.Position = dataOffset;
            int flags = hintStream.ReadByte();
            long bitOffset = ReadBytes(hintStream);
            int blockType = (flags & 3);
            bool isFinal = (flags & 0x80) != 0;
            long treeStart = -1;

            if (blockType == 2)
                treeStart = ReadBytes(hintStream);

            int block0Limit = blockType == 0 ? header.HintInterval : 0;
            if ((flags & 0x04) != 0)
                block0Limit = (int)ReadBytes(hintStream);

            int atomOffset = 0;
            MemoryStream initBuffer = new MemoryStream();

            if ((flags & 0x08) != 0)
            {
                if ((flags & 0x10) != 0)
                    atomOffset = (int)ReadBytes(hintStream);

                int intervalsCount = (int)ReadBytes(hintStream);
                List<int> intervals = new List<int>();
                for (int i = 0; i < intervalsCount; i++)
                {
                    intervals.Add((int)ReadBytes(hintStream));
                }

                byte[] unpacked = UnpackBytes(hintStream);
                initBuffer.Write(unpacked, 0, intervals[0]);
                int j = intervals[0];
                for (int i = 1; i < intervals.Count - 1; i += 2)
                {
                    initBuffer.Write(new byte[intervals[i]], 0, intervals[i]);
                    initBuffer.Write(unpacked, j, intervals[i + 1]);
                    j += intervals[i + 1];
                }
                if ((intervals.Count & 1) == 0)
                {
                    initBuffer.Write(new byte[intervals[intervals.Count - 1]], 0, 
                        intervals[intervals.Count - 1]);
                }
            }

            byte[] buf1 = initBuffer.ToArray();

            MemoryStream outputBytes = new MemoryStream();
            if (atomOffset > 0)
            {
                outputBytes.Write(buf1, buf1.Length - atomOffset, atomOffset);
            }

            if (outputBytes.Length < header.HintInterval)
            {
                DecoderWithInitialization decoder = new DecoderWithInitialization(deflateStream);
                decoder.SetDictionary(buf1, 0, buf1.Length);

                DecoderWithInitialization.InitializationData initData = new DecoderWithInitialization.InitializationData();
                initData.BlockType = blockType;
                initData.IsFinal = isFinal;
                initData.BitPosition = bitOffset;
                initData.InputOffset = header.StreamOffset;

                switch (blockType)
                {
                    case 0:
                        initData.Block0Limit = block0Limit;
                        break;
                    case 2:
                        CodesAndDistances dynamicTrees = decoder.ReadDynamicTrees(header.StreamOffset, treeStart);
                        initData.Block2HuffmanTrees = dynamicTrees;
                        break;
                }
                decoder.SetInitializationData(initData);

                int read;
                byte[] buf = new byte[1024];
                int tail = header.HintInterval - (int)outputBytes.Length;
                while (tail > 0 && (read = decoder.Read(buf, 0, Math.Min(tail, buf.Length))) > 0)
                {
                    outputBytes.Write(buf, 0, read);
                    tail = header.HintInterval - (int)outputBytes.Length;
                }
            }

            return outputBytes.ToArray();
        }
        
        static byte[] UnpackBytes(Stream s)
        {
            const int BufferSize = 1024;
            MemoryStream ms = new MemoryStream();
            byte[] buffer = new byte[BufferSize];
            Decoder decoder = new Decoder(s);
            int read;
            while ((read = decoder.Read(buffer, 0, buffer.Length)) > 0)
            {
                ms.Write(buffer, 0, read);
            }
            return ms.ToArray();
        }

        static long ReadBytes(Stream s)
        {
            int shift = 0;
            long result = 0;
            int data;
            do
            {
                data = s.ReadByte();
                result |= (long)(data & 0x7F) << shift;
                shift += 7;
            } while ((data & 0x80) != 0);
            return result;
        }

        class HeaderInformation
        {
            public int HintInterval;
            public long PointersOffset;
            public long UncompressedSize;
            public long StreamOffset;
        }
    }
}
